/*
 * @author: Duo Ji
 */
package com.server.cm;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.lang.reflect.Type;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Vector;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicLong;

import com.data.AndroidDevice;
import com.data.Device;
import com.data.Interview;
import com.data.Platform;
import com.data.Request;
import com.data.SourceType;
import com.data.User;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import com.server.common.DetailInfoKeys;
import com.server.common.InterviewKeys;
import com.server.common.OpCode;
import com.server.common.RequestKeys;
import com.server.common.ResponseKeys;
import com.server.dao.InterviewDAO;
import com.server.dao.JDBCInterviewDAO;
import com.server.dao.JDBCUserDAO;
import com.server.dao.UserDAO;
import com.util.Debug;

/**
 * The Class RequestHandler.
 */
public class RequestHandler implements Runnable {

	/** The Executor Service. */
	ExecutorService _exe;

	/** The _request socket. */
	ServerSocket _requestSocket;

	/** The _request id. */
	AtomicLong _requestId;

	/**
	 * Instantiates a new request handler.
	 * 
	 * @param numThreads
	 *            the number of threads
	 * @param requestPort
	 *            the request port
	 * @param requestId
	 *            the request id
	 * @throws IOException
	 *             Signals that an I/O exception has occurred.
	 */
	public RequestHandler(int numThreads, int requestPort, AtomicLong requestId) throws IOException {
		_requestId = requestId;
		_exe = Executors.newFixedThreadPool(numThreads);
		// create server socket listening request from clients
		_requestSocket = new ServerSocket(requestPort);
		// SSLServerSocketFactory serverSocketFactory = (SSLServerSocketFactory)
		// SSLServerSocketFactory.getDefault();
		// _requestSocket = (SSLServerSocket) serverSocketFactory.createServerSocket(requestPort,
		// 10);

	}

	/**
	 * Run.
	 */
	public void run() {
		Debug.println("RH -- start run()");
		try {
			// get the IP address of local host, and display it.
			byte[] ipAddr = InetAddress.getLocalHost().getAddress();
			String ipAddrStr = "";
			for (int i = 0; i < ipAddr.length; i++) {
				if (i > 0) {
					ipAddrStr += ".";
				}
				ipAddrStr += ipAddr[i] & 0xFF;
			}
			System.out.println("Please make client apps sending request to address: " + ipAddrStr);
		} catch (IOException e) {
			e.printStackTrace();
		}

		for (;;) {
			try {

				Debug.println("RH -- accept socket connection");
				Socket socket = _requestSocket.accept();
				Debug.println("RH -- received socket connection");
				Runnable woker = new RHWorker(socket);
				_exe.execute(woker);
			} catch (IOException e) {
				e.printStackTrace();
				// This exception could come from RHWorker(socket) or SSLSocket.accept(). Either
				// case, we can ignore and proceed to the next loop
			}
		} // for
	} // run

	/**
	 * The Class RHWoker.
	 */
	private class RHWorker implements Runnable {

		/** The _socket. */
		Socket _socket;

		/** The _output. */
		PrintWriter _output;

		/** The _input. */
		BufferedReader _input;

		/** The _user id. */
		String _userId;

		/** The _user password. */
		String _userPwd;

		/** The UserDAO. */
		UserDAO _udao;

		/** The _gson. */
		Gson _gson;

		/**
		 * Instantiates a new Request Handler Worker.
		 * 
		 * @param s
		 *            the s
		 * @throws IOException
		 *             Signals that an I/O exception has occurred.
		 */
		public RHWorker(Socket s) throws IOException {
			_socket = s;
			_input = new BufferedReader(new InputStreamReader(_socket.getInputStream()));
			_output = new PrintWriter(_socket.getOutputStream(), true);

			// TODO may have to change the DAO initialization
			_udao = new JDBCUserDAO();
			_gson = new Gson();
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see java.lang.Runnable#run()
		 */
		public void run() {
			Debug.println("RHWorker -- run()");
			Gson gson = new Gson();
			String s;
			// get client userId and password
			try {
				Debug.println("RHWorker -- read userInfo");
				s = _input.readLine();

				Debug.println("RHWorker -- message in JSON = " + s);

				Type mapType = new TypeToken<Map<String, String>>() {
				}.getType();
				HashMap<String, String> message = gson.fromJson(s, mapType);
				_userId = message.get(RequestKeys.USER_NAME);
				_userPwd = message.get(RequestKeys.PWD);
				if (_userId == null) {
					// invalid, return error to client
					sendError("Missing userId");
				}
				String op = message.get(RequestKeys.OP_CODE);
				Debug.println("RHWorker -- parsed opCode = " + op);
				OpCode oc = Enum.valueOf(OpCode.class, op);
				switch (oc) {
				case Authenication:
					_userPwd = message.get(RequestKeys.PWD);
					if (_userPwd == null) {
						// invalid, return error to client
						sendError("Missing userId");
					}
					// first time user
					serviceAuth(message);
					break;
				case GetAllInterview:
					// return brief interview info
					syncList(message);
					break;
				case GetOneInterview:
					// return detailed interview
					syncOne(message);
					break;
				case SetSyncInterval:
					// sent sync interval

					break;
				case C2DMRegistration:
					c2dmReg(message);
					break;
				default:
					sendError("Invalid op code");
					break;
				}
			} catch (IOException e) {
				e.printStackTrace();
			}
			clean();
		} // run()

		private void c2dmReg(HashMap<String, String> message) {
			Debug.println("RHWorker -- c2dmReg()");
			String userID = message.get(RequestKeys.USER_NAME);
			int deviceId = Integer.parseInt(message.get(RequestKeys.DEVICE_ID));
			String regID = message.get(RequestKeys.C2DM_REG_ID);
			_udao.saveUserDeviceRegID(userID, deviceId, regID);
			Debug.println("RHWorker -- c2dmReg(): regID = " + regID);
		}

		/**
		 * Service authorization request. Only check if user information is valid. If so, persist
		 * user data and return success message. Return error message, otherwise.
		 * 
		 * @param message
		 *            the message
		 */
		private void serviceAuth(HashMap<String, String> message) {
			Debug.println("RHWorker -- serviceAuth()");

			// create a new Request
			Request r = new Request(_requestId.getAndIncrement(), _userId, _userPwd, SourceType.clientAuth, new Date());
			r.setPlatform(Platform.Android);

			RequestProcessor rp = new RequestProcessor(r);
			// retrieve interviews
			Vector<Interview> interviews = rp.getBackToWork();
			HashMap<String, String> resultMsg = new HashMap<String, String>();
			if (interviews == null) {
				// invalid userId and/or password, send error
				sendError("Invalid userId and/or password");
			} else {
				// valid new user

				User u = new User(r.getUserId(), r.getUserPwd());
				u.setLastCheckedTime(new Date());
				HashMap<String, String> deviceInfo = new HashMap<String, String>();
				AndroidDevice d = new AndroidDevice(_userId, deviceInfo);
				Vector<Device> ds = new Vector<Device>();
				ds.add(d);
				u.addDevice(d);
				u.setPlatform(Platform.Android);
				u.setRetrivalInterval(10);
				// persist new user
				_udao.saveUser(u);
				// persist Interviews

				// TODO may have to change intDAO initialization
				InterviewDAO intDAO = new JDBCInterviewDAO();
				for (int i = 0; i < interviews.size(); i++) {
					// persist Interview Objects
					Interview intv = interviews.get(i);
					intDAO.saveInterview(intv);
				}

				// send success message
				resultMsg.put(ResponseKeys.RESULT, ResponseKeys.SUCCESS);

				// TODO just stuff a dump deviceID for now
				resultMsg.put(ResponseKeys.DEVICE_ID, "0");
				sendString(_gson.toJson(resultMsg));
			}
		} // serviceAuth

		/**
		 * Send error message to device.
		 * 
		 * @param string
		 *            the string
		 */
		private void sendError(String string) {
			HashMap<String, String> resultMsg = new HashMap<String, String>();
			resultMsg.put(ResponseKeys.MSG_TYPE, ResponseKeys.ERROR);
			resultMsg.put(ResponseKeys.MESSAGE, string);
			sendString(_gson.toJson(resultMsg));
		}

		/**
		 * Service sync Interview List request. Return only Interviews that have not sync'ed before.
		 * 
		 * @param message
		 *            the message
		 */
		private void syncList(HashMap<String, String> message) {
			// create a new Request
			Request r = new Request(_requestId.getAndIncrement(), _userId, _userPwd, SourceType.clientSyncList, new Date());
			r.setPlatform(Platform.Android);
			RequestProcessor rp = new RequestProcessor(r);
			Vector<Interview> interviews = rp.getBackToWork();
			if (interviews != null) {
				Debug.println("RHWorker -- syncList(): list not empty");
				for (int i = 0; i < interviews.size(); i++) {
					// for all the interviews, send the information needed to populate the interview
					// list on device
					if (!sendInterviewListItem(interviews.get(i))) {
						sendError("Could not send InterviewList item");
					}
				} // for
			} // if
			sendEnd();
		}

		/**
		 * Send end.
		 */
		private void sendEnd() {
			HashMap<String, String> m = new HashMap<String, String>();
			m.put(ResponseKeys.MSG_TYPE, ResponseKeys.END);
			sendString(_gson.toJson(m));
		}

		/**
		 * Send one item in the interview list.
		 * 
		 * @param i
		 *            the i
		 * @return true, if successful
		 */
		private boolean sendInterviewListItem(Interview i) {
			HashMap<String, String> m = new HashMap<String, String>();
			m.put(ResponseKeys.MSG_TYPE, ResponseKeys.LIST);
			m.put(InterviewKeys.INTERVIEW_ID, i.getJobID());
			m.put(InterviewKeys.LOCATION, i.getLocation());
			m.put(InterviewKeys.POSITION, i.getJobTitle());
			m.put(InterviewKeys.COMPANY_NAME, i.getEmployerName());
			m.put(InterviewKeys.TIME, i.getDate());
			if (!sendString(_gson.toJson(m))) {
				return false;
			}
			return true;
		}

		/**
		 * Service sync one Interview request. jobId must have been filled in message
		 * 
		 * @param message
		 *            the message
		 * @return true, if successful
		 */
		private boolean syncOne(HashMap<String, String> message) {
			String jobId = message.get(InterviewKeys.INTERVIEW_ID);
			Request r = new Request(_requestId.getAndIncrement(), _userId, _userPwd, SourceType.clientSyncList, new Date());
			r.setPlatform(Platform.Android);
			r.setJobId(jobId);
			RequestProcessor rp = new RequestProcessor(r);
			Vector<Interview> v = rp.getBackToWork();
			if (v == null || v.size() == 0) {
				sendError("Could not find interview");
				return false;
			}
			Interview i = v.get(0);
			HashMap<String, String> m = new HashMap<String, String>();
			m.put(ResponseKeys.MSG_TYPE, ResponseKeys.ONE);
			m.put(DetailInfoKeys.TYPE, i.getType());
			m.put(DetailInfoKeys.STATUS, i.getStatus());
			m.put(DetailInfoKeys.LENGTH, String.valueOf(i.getLength()));
			m.put(DetailInfoKeys.JOB_DESC, i.getJobDescription());
			if (!sendString(_gson.toJson(m))) {
				return false;
			}
			return true;
		}

		/**
		 * Send string with SSLSocket to client.
		 * 
		 * @param s
		 *            the string
		 * @return true, if successful false, otherwise
		 */
		private boolean sendString(String s) {
			Debug.println("RHWorker -- sendingString(): " + s);
			if (_socket.isClosed()) {
				return false;
			}
			_output.println(s);
			_output.flush();
			return true;
		}

		/**
		 * Clean.
		 */
		private void clean() {
			Debug.println("RHWorker -- clean()");
			try {
				_input.close();
				_output.flush();
				_output.close();
				_socket.close();
			} catch (IOException e) {
				e.printStackTrace();
			}

		}
	}// class RHWorker
}
